package cloud.seri.auth.config

import cloud.seri.auth.security.AuthoritiesConstants
import io.github.jhipster.config.JHipsterProperties
import org.springframework.beans.BeansException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ClassPathResource
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer
import org.springframework.security.oauth2.provider.token.TokenEnhancer
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain
import org.springframework.security.oauth2.provider.token.TokenStore
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.web.filter.CorsFilter

import javax.servlet.http.HttpServletResponse

@Configuration
@EnableAuthorizationServer
class UaaConfiguration(
    private val jHipsterProperties: JHipsterProperties,
    private val uaaProperties: UaaProperties,
    private val passwordEncoder: PasswordEncoder
) : AuthorizationServerConfigurerAdapter(), ApplicationContextAware {

    private var applicationContext: ApplicationContext? = null

    @Autowired
    @Qualifier("authenticationManagerBean")
    private val authenticationManager: AuthenticationManager? = null

    @Throws(BeansException::class)
    override fun setApplicationContext(applicationContext: ApplicationContext) {
        this.applicationContext = applicationContext
    }

    @EnableResourceServer
    class ResourceServerConfiguration(private val tokenStore: TokenStore, private val jHipsterProperties: JHipsterProperties, private val corsFilter: CorsFilter) : ResourceServerConfigurerAdapter() {

        @Throws(Exception::class)
        override fun configure(http: HttpSecurity) {
            http
                .exceptionHandling()
                .authenticationEntryPoint { _, response, _ -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED) }
                .and()
                .csrf()
                .disable()
                .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter::class.java)
                .headers()
                .frameOptions()
                .disable()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/api/register").permitAll()
                .antMatchers("/api/activate").permitAll()
                .antMatchers("/api/authenticate").permitAll()
                .antMatchers("/api/account/reset-password/init").permitAll()
                .antMatchers("/api/account/reset-password/finish").permitAll()
                .antMatchers("/api/**").authenticated()
                .antMatchers("/management/health").permitAll()
                .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
                .antMatchers("/v2/api-docs/**").permitAll()
                .antMatchers("/swagger-resources/configuration/ui").permitAll()
                .antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN)
        }

        @Throws(Exception::class)
        override fun configure(resources: ResourceServerSecurityConfigurer?) {
            resources!!.resourceId("jhipster-uaa").tokenStore(tokenStore)
        }
    }

    @Throws(Exception::class)
    override fun configure(clients: ClientDetailsServiceConfigurer?) {
        var accessTokenValidity = uaaProperties.webClientConfiguration.accessTokenValidityInSeconds
        accessTokenValidity = Math.max(accessTokenValidity, MIN_ACCESS_TOKEN_VALIDITY_SECS)
        var refreshTokenValidity = uaaProperties.webClientConfiguration.refreshTokenValidityInSecondsForRememberMe
        refreshTokenValidity = Math.max(refreshTokenValidity, accessTokenValidity)
        /*
        For a better client design, this should be done by a ClientDetailsService (similar to UserDetailsService).
         */
        clients!!.inMemory()
            .withClient(uaaProperties.webClientConfiguration.clientId)
            .secret(passwordEncoder.encode(uaaProperties.webClientConfiguration.secret))
            .scopes("openid")
            .autoApprove(true)
            .authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code")
            .accessTokenValiditySeconds(accessTokenValidity)
            .refreshTokenValiditySeconds(refreshTokenValidity)
            .and()
            .withClient(jHipsterProperties.security.clientAuthorization.clientId)
            .secret(passwordEncoder.encode(jHipsterProperties.security.clientAuthorization.clientSecret))
            .scopes("web-app")
            .authorities("ROLE_ADMIN")
            .autoApprove(true)
            .authorizedGrantTypes("client_credentials")
            .accessTokenValiditySeconds(jHipsterProperties.security.authentication.jwt.tokenValidityInSeconds.toInt())
            .refreshTokenValiditySeconds(jHipsterProperties.security.authentication.jwt.tokenValidityInSecondsForRememberMe.toInt())
    }

    @Throws(Exception::class)
    override fun configure(endpoints: AuthorizationServerEndpointsConfigurer?) {
        // pick up all  TokenEnhancers incl. those defined in the application
        // this avoids changes to this class if an application wants to add its own to the chain
        val tokenEnhancers = applicationContext!!.getBeansOfType(TokenEnhancer::class.java).values
        val tokenEnhancerChain = TokenEnhancerChain()
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers.toList())
        endpoints!!
            .authenticationManager(authenticationManager)
            .tokenStore(tokenStore())
            .tokenEnhancer(tokenEnhancerChain)
            .reuseRefreshTokens(false) // don't reuse or we will run into session inactivity timeouts
    }

    /**
     * Apply the token converter (and enhancer) for token store.
     * @return the [JwtTokenStore] managing the tokens.
     */
    @Bean
    fun tokenStore(): JwtTokenStore {
        return JwtTokenStore(jwtAccessTokenConverter())
    }

    /**
     * This bean generates an token enhancer, which manages the exchange between JWT acces tokens and Authentication
     * in both directions.
     *
     * @return an access token converter configured with the authorization server's public/private keys.
     */
    @Bean
    fun jwtAccessTokenConverter(): JwtAccessTokenConverter {
        val converter = JwtAccessTokenConverter()
        val keyPair = KeyStoreKeyFactory(
            ClassPathResource(uaaProperties.keyStore.name), uaaProperties.keyStore.password.toCharArray())
            .getKeyPair(uaaProperties.keyStore.alias)
        converter.setKeyPair(keyPair)
        return converter
    }

    @Throws(Exception::class)
    override fun configure(oauthServer: AuthorizationServerSecurityConfigurer?) {
        oauthServer!!.tokenKeyAccess("permitAll()").checkTokenAccess(
            "isAuthenticated()")
    }

    companion object {
        /**
         * Access tokens will not expire any earlier than this.
         */
        private const val MIN_ACCESS_TOKEN_VALIDITY_SECS = 60
    }
}
